package com.kvn.interceptor;

import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.expr.SQLIdentifierExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlInsertStatement;
import com.alibaba.druid.sql.dialect.mysql.visitor.MySqlSchemaStatVisitor;
import com.alibaba.druid.stat.TableStat;
import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.collect.*;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.util.CollectionUtils;

import javax.annotation.Nullable;
import java.util.*;

/**
 * sql解析器
 * Created by wangzhiyuan on 2018/6/11
 */
public class SqlParser {
    private String sql;
    /**
     * sql中的参数值对象
     */
    private Object parameterObject;
    private MappedStatement mappedStatement;
    private BoundSql boundSql;
    private List<SQLStatement> sqlStatements;

    public SqlParser(String sql, Object parameterObject, MappedStatement mappedStatement, BoundSql boundSql, List<SQLStatement> sqlStatements) {
        this.sql = sql;
        this.parameterObject = parameterObject;
        this.mappedStatement = mappedStatement;
        this.boundSql = boundSql;
        this.sqlStatements = sqlStatements;
    }

    /**
     * 解析 sql
     *
     * @return
     */
    public List<SqlParserResult> parse() {
        List<ParameterMapping> paramMappings = boundSql.getParameterMappings();
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        SQLStatement sqlStatement = sqlStatements.get(0);

        LinkedList<ParameterMapping> paramMappingsQueue = new LinkedList(paramMappings);
        if (SqlCommandType.INSERT == sqlCommandType) {
            return parseWithoutCondition((MySqlInsertStatement) sqlStatement, paramMappingsQueue);
        }

        if (SqlCommandType.DELETE == sqlCommandType) {
            return parseWithConditions(sqlStatement, paramMappingsQueue);
        }

        if (SqlCommandType.UPDATE == sqlCommandType) {
            return parseWithConditions(sqlStatement, paramMappingsQueue);
        }

        if (SqlCommandType.SELECT == sqlCommandType) {
            return distinct(processColumnWithoutValue(parseWithConditions(sqlStatement, paramMappingsQueue)));
        }

        throw new RuntimeException("不支持的 Sql 类型，sql:" + sql);
    }

    /**
     * 去重
     * @param sqlParserResults
     * @return
     */
    private List<SqlParserResult> distinct(List<SqlParserResult> sqlParserResults) {
        ArrayList<SqlParserResult> distinctList = Lists.newArrayList();

        ImmutableListMultimap<TableStat.Column, SqlParserResult> multimap = Multimaps.index(sqlParserResults, new Function<SqlParserResult, TableStat.Column>() {
            public TableStat.Column apply(SqlParserResult sqlParserResult) {
                return new TableStat.Column(sqlParserResult.getTableName(), sqlParserResult.getColumn());
            }
        });

        ImmutableSet<TableStat.Column> keys = multimap.keySet();
        for (TableStat.Column key : keys) {
            ImmutableList<SqlParserResult> immutableList = multimap.get(key);
            distinctList.add(immutableList.get(0));
        }

        return distinctList;
    }

    /**
     * 处理没有值的字段：取与之等价的字段的值
     * @param sqlParserResults
     * @return
     */
    private List<SqlParserResult> processColumnWithoutValue(List<SqlParserResult> sqlParserResults) {
        Multimap<TableStat.Column, SqlParserResult> columnMultiMap = Multimaps.index(sqlParserResults, new Function<SqlParserResult, TableStat.Column>() {
            public TableStat.Column apply(SqlParserResult sqlParserResult) {
                return new TableStat.Column(sqlParserResult.getTableName(), sqlParserResult.getColumn());
            }
        });
        for (SqlParserResult result : sqlParserResults) {
            if (result.getColumnValues() != null) {
                continue;
            }

            if (result.getEqualColumn() == null) {
                throw new RuntimeException(String.format("%s.%s没有值，也没有与之等价的字段", result.getTableName(), result.getColumn()));
            }


            Collection<SqlParserResult> equalResults = columnMultiMap.get(result.getEqualColumn());
            for (SqlParserResult equalResult : equalResults) {
                if (equalResult.getColumnValues() != null) {
                    result.setColumnValues(equalResult.getColumnValues());
                    break;
                }
            }

            if (result.getColumnValues() == null) {
                Collection<SqlParserResult> equalResults2 = columnMultiMap.get(new TableStat.Column(result.getTableName(), result.getColumn()));
                for (SqlParserResult equalResult : equalResults2) {
                    if (equalResult.getColumnValues() != null) {
                        result.setColumnValues(equalResult.getColumnValues());
                        break;
                    }
                }
            }


            if (result.getColumnValues() == null) {
                throw new RuntimeException(String.format("没有找到%s对应的值", result.toString()));
            }
        }
        return sqlParserResults;
    }

    private List<SqlParserResult> parseWithoutCondition(MySqlInsertStatement sqlStatement, LinkedList<ParameterMapping> paramMappingsQueue) {
        String tableName = sqlStatement.getTableName().getSimpleName();
        List<SQLExpr> columns = sqlStatement.getColumns();
        List<SqlParserResult> parserResultList = Lists.newArrayList();
        for (int i = 0; i < columns.size(); i++) {
            SQLIdentifierExpr expr = (SQLIdentifierExpr) columns.get(i);
            SqlParserResult result = new SqlParserResult();
            result.setTableName(tableName);
            result.setColumn(expr.getName());
            Object paramValue = getParamValue(paramMappingsQueue.pop());
            result.setColumnValues(Lists.newArrayList(paramValue));
            parserResultList.add(result);
        }

        return parserResultList;
    }

    private List<SqlParserResult> parseWithConditions(SQLStatement sqlStatement, LinkedList<ParameterMapping> paramMappingsQueue) {
        MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
        sqlStatement.accept(visitor);
        List<SqlParserResult> parserResultList = new ArrayList<>();
        List<TableStat.Condition> conditions = visitor.getConditions();

        if (CollectionUtils.isEmpty(conditions)) {
            throw new RuntimeException("sql中不含分表字段：" + sql);
        }

        for (int i = 0; i < conditions.size(); i++) {
            TableStat.Condition condition = conditions.get(i);
            SqlParserResult rlt = parseConditionColumn(condition, paramMappingsQueue, visitor);
            if (rlt != null) {
                parserResultList.add(rlt);
            }
        }
        return parserResultList;
    }

    private SqlParserResult parseConditionColumn(TableStat.Condition condition, LinkedList<ParameterMapping> paramMappingsQueue, MySqlSchemaStatVisitor visitor) {
        SqlParserResult result = new SqlParserResult();
        result.setTableName(condition.getColumn().getTable());
        result.setColumn(condition.getColumn().getName());

        // 联接查询条件：例如table1.id=table2.id
        List<Object> values = condition.getValues();
        if (CollectionUtils.isEmpty(values)) {
            // 两个表之间做内联接的支持
            TableStat.Column equalColumn = findEqualColumn(condition.getColumn(), visitor);
            result.setEqualColumn(equalColumn);
            return result;
        }

        // paramMappingsQueue 需要poll的个数
        int needPoll = values.size();
        Collection<Object> valuesAfterfilter = Collections2.filter(values, Predicates.notNull());
        // 排除不带?的参数个数
        needPoll = needPoll - valuesAfterfilter.size();

        // 如果是直接写在SQL中的参数
        if (!CollectionUtils.isEmpty(valuesAfterfilter)) {
            result.setColumnValues(values);
            return result;
        }

        // 如果是带?的参数
        List paramValueList = new ArrayList();
        for (int i = 0; i < needPoll; i++) {
            Object paramValue = getParamValue(paramMappingsQueue.pop());
            paramValueList.add(paramValue);
        }
        result.setColumnValues(paramValueList); // 参数值多于1个，用List存储
        return result;
    }

    /**
     * 找出关联的字段
     * @param column
     * @param visitor
     * @return
     */
    private TableStat.Column findEqualColumn(TableStat.Column column, MySqlSchemaStatVisitor visitor) {
        Set<TableStat.Relationship> relationships = visitor.getRelationships();
        for (TableStat.Relationship relationship : relationships) {
            if (!"=".equals(relationship.getOperator())) {
                continue;
            }
            if (column.equals(relationship.getLeft())) {
                return relationship.getRight();
            }
            if (column.equals(relationship.getRight())) {
                return relationship.getLeft();
            }
        }

        throw new RuntimeException("没有找到与column相等的字段，column:" + column);
    }


    /**
     * 如果是带?的参数，获取它的值
     *
     * @return
     * @see org.apache.ibatis.scripting.defaults.DefaultParameterHandler
     */
    private Object getParamValue(ParameterMapping parameterMapping) {
        Configuration configuration = mappedStatement.getConfiguration();
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();

        if (parameterMapping.getMode() != ParameterMode.OUT) {
            Object value;
            String propertyName = parameterMapping.getProperty();
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }

            return value;
        }

        return null;
    }


}
